package org.feisoft.jta.supports.jdbc;

import com.alibaba.fastjson.JSON;
import com.google.common.base.Joiner;
import drds.global_transaction.image.UndoLog;
import drds.global_transaction.image.image_praser.AbstractImagePraser;
import drds.global_transaction.image.image_praser.InsertImagePraser;
import drds.global_transaction.image.image_praser.SqlPraser;
import drds.global_transaction.lock.ExclusiveLock;
import drds.global_transaction.lock.Lock;
import drds.global_transaction.lock.ShareILock;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.Select;
import net.sf.jsqlparser.statement.select.SelectBody;
import org.apache.commons.lang3.StringUtils;
import org.feisoft.common.utils.DbPool.Db;
import org.feisoft.common.utils.SpringBeanUtil;
import org.feisoft.jta.TransactionImpl;
import org.feisoft.transaction.TransactionBeanFactory;
import org.feisoft.transaction.archive.XAResourceArchive;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;

import javax.sql.XAConnection;
import javax.transaction.Status;
import javax.transaction.xa.XAException;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

//import drds.global_transaction.image.resolvers.AbstractImagePraser;
//import drds.global_transaction.image.resolvers.InsertImagePraser;

public class DynamicPreparedStatementProxyHandler implements InvocationHandler
{

    private static final char[] HEX_DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd',
            'e', 'f'};
    public Xid currentXid;
    Logger logger = LoggerFactory.getLogger(DynamicPreparedStatementProxyHandler.class);
    List<String> proxyUpdateMethods = Arrays
            .asList("executeUpdate", "execute", "executeBatch", "executeLargeBatch", "executeLargeUpdate");
    private Db db = null;
    private Object realObject;
    private String sql;
    private XAConnection xaConnection;
    private String gloableTransactionId;
    private String branchTransactionId;
    private List<Object> params = new ArrayList<>();
    private long timeOut = 2 * 1000;

    public DynamicPreparedStatementProxyHandler(Object realObject, String sql, XAConnection xaConnection)
    {
        this.realObject = realObject;
        this.sql = sql;
        this.xaConnection = xaConnection;
        if (this.db == null)
        {
            Db db = (Db) SpringBeanUtil.getBean("db");
            this.db = db;
        }
    }

    private static void appendAsHex(StringBuilder builder, byte[] bytes)
    {
        builder.append("0x");
        for (byte b : bytes)
        {
            builder.append(HEX_DIGITS[(b >>> 4) & 0xF]).append(HEX_DIGITS[b & 0xF]);
        }
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
    {

        TransactionBeanFactory transactionBeanFactory = (TransactionBeanFactory) SpringBeanUtil.getBean("jtaBeanFactory");
        TransactionImpl transaction = (TransactionImpl) transactionBeanFactory.getTransactionManager().getTransaction();

        if (transaction == null || "close".equals(method.getName()))
        {
            return method.invoke(realObject, args);
        }

        if (method.getName().startsWith("set") && args != null && args.length == 2)
        {
            params.add(args[1]);
        }
        if (Status.STATUS_ACTIVE != transaction.getStatus())
        {
            throw new SQLException("Operation is disabled during the inactive phase of the transaction!");
        } else if (!transaction.isTiming())
        {
            throw new SQLException(
                    "ILock operation is disabled during the inactive phase of the transaction when transaction is stoped!");
        }

        if (isQueryMethod(method) || isUpdateMethod(method.getName()))
        {
            if (currentXid == null)
            {
                List<XAResourceArchive> nativeParticipantList = transaction.getNativeParticipantList();
                if (nativeParticipantList.size() > 0)
                {
                    currentXid = nativeParticipantList.get(0).getXid();
                }
            }
            if (currentXid != null)
            {
                gloableTransactionId = gloableTransactionId(currentXid);
                branchTransactionId = branchTransactionId(currentXid);
            }

        }

        if (isUpdateMethod(method.getName()))
        {
            if (currentXid == null)
            {
                //                    logger.error("method.getName()={},args={}-----没有xid-----", method.getName(), args);
                throw new SQLException("No gloableTransactionId");
            }
            return invokUpdate(method, args, transaction);
        }

        if (isQueryMethod(method))
        {

            try
            {
                if (args == null || StringUtils.isEmpty(args[0].toString()))
                {
                    //select x
                    return method.invoke(realObject, args);
                }
                sql = args[0].toString();
                if (StringUtils.deleteWhitespace(sql.toLowerCase()).endsWith("lockinsharemode"))
                {
                    return shareLockMode(method, args);
                } else
                {
                    net.sf.jsqlparser.statement.Statement statement;
                    try
                    {
                        statement = CCJSqlParserUtil.parse(sql);
                    } catch (JSQLParserException e)
                    {
                        logger.error("jsqlparser.praseFailed,sql=" + sql);
                        return method.invoke(realObject, args);
                    }
                    Select select = (Select) statement;
                    SelectBody selectBody = select.getSelectBody();
                    if (selectBody instanceof PlainSelect)
                    {
                        PlainSelect plainSelect = (PlainSelect) selectBody;
                        if (plainSelect.isForUpdate())
                        {
                            // update
                            return lockForUpdate(method, args);
                        }
                    }

                }
            } finally
            {
                xaCommit(method, args);
            }
        }
        return method.invoke(realObject, args);

    }

    private boolean isQueryMethod(Method method)
    {
        return "executeQuery".equals(method.getName());
    }

    private boolean isUpdateMethod(String methodName)
    {
        return proxyUpdateMethods.contains(methodName);
    }

    private Object lockForUpdate(Method method, Object[] args)
            throws SQLException, JSQLParserException, IllegalAccessException, InvocationTargetException
    {
        UndoLog undoLog = new UndoLog();
        try
        {

            AbstractImagePraser abstractImagePraser = AbstractImagePraser.newImagePraser(sql, undoLog);
            undoLog.setBeforeImage(abstractImagePraser.generateBeforeImage());
            getExclusiveLock(abstractImagePraser, gloableTransactionId, branchTransactionId, null);
        } finally
        {
            xaCommit(method, args);
        }

        return method.invoke(realObject, args);
    }

    private Object shareLockMode(Method method, Object[] args)
            throws SQLException, JSQLParserException, IllegalAccessException, InvocationTargetException
    {
        UndoLog undoLog = new UndoLog();

        Object obj;
        try
        {
            AbstractImagePraser resolver = AbstractImagePraser
                    .newImagePraser(sql.substring(0, sql.toLowerCase().indexOf("lock")), undoLog);
            undoLog.setBeforeImage(resolver.generateBeforeImage());

            getShareLock(resolver, gloableTransactionId, branchTransactionId);

            obj = method.invoke(realObject, args);
        } finally
        {
            xaCommit(method, args);
        }

        return obj;
    }

    private Object invokUpdate(Method method, Object[] args, TransactionImpl transaction)
            throws SQLException, JSQLParserException, IllegalAccessException, InvocationTargetException
    {
        UndoLog undoLog = new UndoLog();
        Object obj = null;
        Object primaryKeyColumnValue = null;

        if (realObject instanceof PreparedStatement)
        {
            sql = printRealSql(sql, params);
        } else if (realObject instanceof Statement)
        {

            sql = args[0].toString();
        }
        //事务数据源从对应数据库获取前置对象

        AbstractImagePraser imagePraser;
        List<Lock> lockedLockList;
        try
        {
            imagePraser = AbstractImagePraser.newImagePraser(sql, undoLog);
            undoLog.setBeforeImage(imagePraser.generateBeforeImage());
            lockedLockList = getExclusiveLock(imagePraser, gloableTransactionId, branchTransactionId, transaction);
            if (SqlPraser.insert(sql))
            {
                ResultSet resultSet = null;
                if (realObject instanceof PreparedStatement)
                {
                    if (transaction.isTiming())
                    {
                        obj = method.invoke(realObject, args);
                    } else
                    {
                        if (lockedLockList.size() > 0)
                        {
                            releaseLock(lockedLockList);
                        }
                        throw new SQLException(
                                "ILock operation is disabled during the inactive phase of the transaction when transaction is stoped!");
                    }
                    PreparedStatement preparedStatement = (PreparedStatement) realObject;
                    resultSet = preparedStatement.getGeneratedKeys();
                } else if (realObject instanceof Statement)
                {
                    Statement realSt = (Statement) realObject;
                    if (transaction.isTiming())
                    {
                        obj = realSt.executeUpdate(sql, Statement.RETURN_GENERATED_KEYS);
                    } else
                    {
                        if (lockedLockList.size() > 0)
                        {
                            releaseLock(lockedLockList);
                        }
                        throw new SQLException(
                                "ILock operation is disabled during the inactive phase of the transaction when transaction is stoped!");
                    }
                    resultSet = realSt.getGeneratedKeys();
                }
                if (resultSet != null)
                {
                    while (resultSet.next())
                    {
                        primaryKeyColumnValue = resultSet.getObject(1);
                    }
                    if (!resultSet.isClosed())
                    {
                        resultSet.close();
                    }
                }

                if (primaryKeyColumnValue == null)
                {
                    String primaryKeyColumnName = imagePraser.getPrimaryKeyColumnName(SqlPraser.insertSentenceTableName(sql));
                    List<String> insertSentenceColumnNameList = SqlPraser.insertSentenceColumnNameList(sql);
                    List<String> insertSentenceColumnValueList = SqlPraser.insertSentenceColumnValueList(sql);
                    if (insertSentenceColumnNameList.contains(primaryKeyColumnName))
                    {
                        primaryKeyColumnValue = insertSentenceColumnValueList.get(insertSentenceColumnNameList.indexOf(primaryKeyColumnName));
                    }
                }
            } else
            {
                if (transaction.isTiming())
                {
                    obj = method.invoke(realObject, args);
                } else
                {
                    releaseLock(lockedLockList);
                    throw new SQLException("ILock operation is disabled during the inactive phase of the transaction!");
                }
            }
        } catch (SQLException e)
        {
            //            logger.error("unhandle Exception", e);
            throw e;
        } finally
        {
            xaCommit(method, args);

        }

        //插入时需要获取主键的value
        if (SqlPraser.insert(sql))
        {
            InsertImagePraser insertImagePraser = (InsertImagePraser) imagePraser;
            insertImagePraser.setPrimaryKeyColumnValue(primaryKeyColumnValue);
            undoLog.setAfterImage(insertImagePraser.generateAfterImage());
        } else
        {
            undoLog.setAfterImage(imagePraser.generateAfterImage());
        }
        String undoLogJson = JSON.toJSONString(undoLog);
        String logSql = "INSERT INTO txc_undo_log (gmt_create,gmt_modified,gloableTransactionId,branch_id,rollback_info,status,server) VALUES(now(),now(),?,?,?,?,?)";
        if (obj != null)
        {
            if (!transaction.isTiming())
            {
                logger.error("transaction is end,but insert logsql");
                if (lockedLockList.size() > 0)
                {
                    logger.info("releaseLock log,sql={},args={}", sql, args);
                    releaseLock(lockedLockList);
                }
                try
                {
                    undoLog.rollback();
                } catch (SQLException e)
                {
                    logger.error("UndoLog.rollback(),error", e);
                    throw e;
                }
            } else
            {
                logger.info("before executeUpdate logsql,transaction isTiming=" + transaction.isTiming());
                db.executeUpdate(logSql, gloableTransactionId, branchTransactionId, undoLogJson, 0, getHost());
                logger.info("logsql exe time={}", System.currentTimeMillis());
            }
        }
        //事务数据源从对应数据库获取后置对象
        return obj;
    }

    private void xaCommit(Method method, Object[] args)
    {
        XAResource xaResource;
        try
        {
            //本地直接提交
            xaResource = xaConnection.getXAResource();
            try
            {
                xaResource.end(currentXid, XAResource.TMSUCCESS);
            } catch (XAException e)
            {
            }
            try
            {
                xaResource.prepare(currentXid);
            } catch (XAException e)
            {
            }
            xaResource.commit(currentXid, false);
        } catch (Exception ex)
        {
            logger.info("Local multi sqls exe!method={},args={}", method.getName(), args);
        }
    }

    private String getHost() throws SQLException
    {
        Connection connection = db.getConnection();
        DatabaseMetaData databaseMetaData = connection.getMetaData();
        String url = databaseMetaData.getURL();
        String host = "";
        Pattern pattern = Pattern.compile("(?<=//|)((\\w)+\\.)+\\w+");
        Matcher matcher = pattern.matcher(url);
        if (matcher.find())
        {
            host = matcher.group();
        }
        db.close(connection, null, null);
        return host;
    }

    private List<Lock> getExclusiveLock(AbstractImagePraser resolver, String gloableTransactionId, String branchTransactionId,
                                        TransactionImpl transaction) throws JSQLParserException, SQLException
    {

        Assert.notNull(transaction, "Transaction must not be null");
        long startDateTimeLong = System.currentTimeMillis();
        long endDateTimeLong = startDateTimeLong;
        while ((endDateTimeLong - startDateTimeLong <= this.timeOut) && transaction.isTiming())
        {
            endDateTimeLong = System.currentTimeMillis();
            List<Lock> lockList = this.getExclusiveLock(gloableTransactionId, branchTransactionId, resolver);
            if (this.lock(lockList))
            {
                return lockList;
            }
            try
            {
                Thread.sleep(100);
            } catch (InterruptedException e)
            {
            }
        }
        throw new SQLException("Proxy.getLockTimeout");

    }

    private boolean releaseLock(List<Lock> lockedLockList) throws SQLException
    {
        return this.release(lockedLockList);
    }

    private void getShareLock(AbstractImagePraser resolver, String gloableTransactionId, String branchTransactionId)
            throws JSQLParserException, SQLException
    {

        long atime = System.currentTimeMillis();

        long btime;
        do
        {
            btime = System.currentTimeMillis();
            List<Lock> lockList = getShareLock(gloableTransactionId, branchTransactionId, resolver, sql);
            if (lock(lockList))
                return;

        } while (btime - atime <= this.timeOut);
        throw new SQLException("Proxy.getLockTimeout");
    }

    private boolean lock(List<Lock> lockList)
    {
        if (lockList.size() > 0)
        {
            for (Lock lock : lockList)
            {
                try
                {
                    if (!lock.isLock())
                    {
                        lock.lock();
                    }
                } catch (Exception e)
                {
                    logger.info("getExclusiveLock -- Data locked by other,retry");
                    return false;
                }
                lock.setLock(true);
            }
        }
        return true;
    }

    private boolean release(List<Lock> lockList) throws SQLException
    {
        if (lockList.size() > 0)
        {
            for (Lock lock : lockList)
            {
                try
                {
                    if (lock.isLock())
                    {
                        lock.unlock();
                    }
                } catch (SQLException e)
                {
                    logger.error("release error", e);
                    throw e;
                }
                lock.setLock(false);
            }
        }
        return true;
    }

    private List<Lock> getExclusiveLock(String gloableTransactionId, String branchTransactionId, AbstractImagePraser imagePraser)
            throws JSQLParserException, SQLException
    {

        String beforeLockSql = imagePraser.getBeforeOrAfterImageSql();
        String primaryKeyColumnName = imagePraser.getPrimaryKeyColumnName(imagePraser.getTableName());

        List<String> allLockList = db
                .executeQuery(beforeLockSql, rs -> rs.getObject(primaryKeyColumnName).toString(), null);
        List<Lock> lockList = new ArrayList<>();

        if (allLockList.size() == 0)
        {
            return lockList;
        }
        String lockSql = "select key_value from txc_lock where gloableTransactionId='" + gloableTransactionId + "'  and branch_id ='" + branchTransactionId
                + "' and table_name = '" + imagePraser.getTableName() + "' and key_value in(" + Joiner
                .on(",")
                .skipNulls()
                .join(allLockList)
                + ")";

        List<String> lockedLockList = db.executeQuery(lockSql, rs -> rs.getObject("key_value").toString(), null);

        allLockList.removeAll(lockedLockList);
        for (String primaryKeyColumnValue : allLockList)
        {

            ExclusiveLock exclusiveLock = new ExclusiveLock();
            exclusiveLock.setLock(Boolean.FALSE);
            exclusiveLock.setGloableTransactionId(gloableTransactionId);
            exclusiveLock.setBranchTransactionId(branchTransactionId);
            exclusiveLock.setTableName(imagePraser.getTableName());
            exclusiveLock.setExclusiveLock("1");
            exclusiveLock.setShareLock(0);
            exclusiveLock.setPrimaryKeyColumnValue(primaryKeyColumnValue);
            exclusiveLock.setCreateDateTimeLong(System.currentTimeMillis());
            lockList.add(exclusiveLock);
        }

        return lockList;
    }

    private List<Lock> getShareLock(String gloableTransactionId, String branchTransactionId, AbstractImagePraser resolver, String sql)
            throws JSQLParserException, SQLException
    {

        String beforeLockSql = resolver.getBeforeOrAfterImageSql();

        String primaryKeyColumnName = resolver.getPrimaryKeyColumnName(resolver.getTableName());
        List<String> allLockList = db
                .executeQuery(beforeLockSql, rs -> rs.getObject(primaryKeyColumnName).toString(), null);

        List<Lock> lockList = new ArrayList<>();

        String lockSql =
                "select key_value,count(*) as count from txc_lock where gloableTransactionId='" + gloableTransactionId + "'  and branch_id ='"
                        + branchTransactionId + "' and table_name = '" + resolver.getTableName() + "' and key_value in(" + Joiner
                        .on(",")
                        .skipNulls()
                        .join(allLockList) + ") group by key_value";
        List<String> lockedLockList = db.executeQuery(lockSql, rs ->
        {
            String tmp = null;
            if (rs.getInt("count") > 1)
                tmp = rs.getString("key_value");
            return tmp;
        }, null);

        allLockList.removeAll(lockedLockList);
        for (String primaryKeyColumnValue : allLockList)
        {

            ShareILock shareLock = new ShareILock();
            shareLock.setLock(Boolean.FALSE);
            shareLock.setGloableTransactionId(gloableTransactionId);
            shareLock.setBranchTransactionId(branchTransactionId);
            shareLock.setTableName(resolver.getTableName());
            shareLock.setExclusiveLock("1");
            shareLock.setShareLock(1);
            shareLock.setPrimaryKeyColumnValue(primaryKeyColumnValue);
            shareLock.setCreateDateTimeLong(System.currentTimeMillis());
            lockList.add(shareLock);
        }

        return lockList;
    }

    /**
     * @param sql        SQL 语句，可以带有 ? 的占位符
     * @param objectList 插入到 SQL 中的参数，可单个可多个可不填
     * @return 实际 sql 语句
     */
    private String printRealSql(String sql, List<Object> objectList)
    {
        if (objectList == null || objectList.size() == 0)
        {
            return sql;
        }

        if (!match(sql, objectList))
        {
            logger.error("SQL 语句中的占位符与参数个数不匹配。SQL：" + sql);
            return null;
        }

        int size = objectList.size();
        Object[] values = new Object[size];
        objectList.toArray(values);
        for (int i = 0; i < size; i++)
        {
            Object value = values[i];
            if (value instanceof Date || value instanceof Timestamp || value instanceof String
                    || value instanceof Blob)
            {
                values[i] = "'" + value + "'";
            } else if (value instanceof Boolean)
            {
                values[i] = (Boolean) value ? 1 : 0;
            }
        }

        return String.format(sql.replaceAll("\\?", "%s"), values);
    }

    /**
     * ? 和参数的实际个数是否匹配
     *
     * @param sql        SQL 语句，可以带有 ? 的占位符
     * @param objectList 插入到 SQL 中的参数，可单个可多个可不填
     * @return true 表示为 ? 和参数的实际个数匹配
     */
    private boolean match(String sql, List<Object> objectList)
    {
        if (objectList == null || objectList.size() == 0)
            return true; // 没有参数，完整输出

        Matcher matcher = Pattern.compile("(\\?)").matcher(sql);
        int count = 0;
        while (matcher.find())
        {
            count++;
        }

        return count == objectList.size();
    }

    private String gloableTransactionId(Xid xid)
    {

        byte[] gtrid = xid.getGlobalTransactionId();

        StringBuilder builder = new StringBuilder();

        if (gtrid != null)
        {
            appendAsHex(builder, gtrid);
        }

        return builder.toString();
    }

    private String branchTransactionId(Xid xid)
    {

        byte[] btrid = xid.getBranchQualifier();

        StringBuilder builder = new StringBuilder();

        if (btrid != null)
        {
            appendAsHex(builder, btrid);
        }

        return builder.toString();
    }

}
